cats effect IO.pureとdelayの違い
cats.effect.IOの#delayと#pureの違いについて1日で2回話したので書きました。
はじめに
cats Effect IOの#delay
と#pure
使い方の誤りについて、たまたま同じ日に同じ指摘を2回したので記事にしてみることにしました。
何が違うのか
IO.delay(とそのエイリアスのIO.apply)は以下のように定義されています。
/** * Suspends a synchronous side effect in `IO`. * * Alias for `IO.delay(body)`. */ def apply[A](body: => A): IO[A] = delay(body) /** * Suspends a synchronous side effect in `IO`. * * Any exceptions thrown by the effect will be caught and sequenced * into the `IO`. */ def delay[A](body: => A): IO[A] = Delay(body _)
対してIO.pureは下記の通りです。
/** * Suspends a pure value in `IO`. * * This should ''only'' be used if the value in question has * "already" been computed! In other words, something like * `IO.pure(readLine)` is most definitely not the right thing to do! * However, `IO.pure(42)` is correct and will be more efficient * (when evaluated) than `IO(42)`, due to avoiding the allocation of * extra thunks. */ def pure[A](a: A): IO[A] = Pure(a)
どちらもAをとってIO[A]を返しますが評価戦略が異なります。delayは非正格(名前渡しパラメータ)ですがpureは正格です。以下のように実行してみると違いが明確です。
import cats.effect.IO import cats.implicits._ @SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements")) object IOExample extends App { //IO.apply println("[IO.apply]") val ioWithApply = List( IO(println("hello")), IO(println("cats")), IO(println("world")) ).sequence ioWithApply.unsafeRunSync() ioWithApply.unsafeRunSync() //IO.pure println("[IO.pure]") val ioWithPure = List( IO(println("hello")), IO.pure(println("cats")), IO(println("world")) ).sequence ioWithPure.unsafeRunSync() ioWithPure.unsafeRunSync() }
実行結果は下記のようになります。
[IO.apply] hello cats world hello cats world [IO.pure] cats hello world hello world
pureを使った2つ目のパターンでは以下の違いがあります。
- 1回目のunsafeRunSyncではhelloとcatsの出力順序が逆になっている
- 2回目のunsafeRunSyncではcatsは出力されない
これはIOが生成されるときに値が評価されているか、IOの実行時に評価されているか、の違いによるものです。
エラー処理
評価タイミングの違いはエラーハンドリングでは致命的な違いをもたらします。
以下のコードではIOの実行時発生したエラーをhandleErrorWithでハンドリングしようという意図で書かれています。
//エラーハンドリング def catsNameOrError: String = throw new RuntimeException("Cats Name Error !!") //IO.apply println("[IO.apply]") IO(catsNameOrError) .handleErrorWith { case e: Throwable => println("Error handled: " + e.getMessage) IO.pure(e.getMessage) }.unsafeRunSync() //IO.pure println("[IO.pure]") IO.pure(catsNameOrError).handleErrorWith { case e: Throwable => println("Error handled: " + e.getMessage) IO.pure(e.getMessage) }.unsafeRunSync()
しかし下記の実行例の通り実際にはIO.pureを使った方はIOが実行される前に例外が送出されるため補足されない例外はmainへ伝搬します。
[IO.apply] Error handled: Cats Name Error !! [IO.pure] Exception in thread "main" java.lang.RuntimeException: Cats Name Error !! at IOExample$.catsNameOrError(IOExample.scala:30) at IOExample$.delayedEndpoint$IOExample$1(IOExample.scala:44) at IOExample$delayedInit$body.apply(IOExample.scala:5) at scala.Function0.apply$mcV$sp(Function0.scala:39) at scala.Function0.apply$mcV$sp$(Function0.scala:39) at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17) at scala.App.$anonfun$main$1$adapted(App.scala:80) at scala.collection.immutable.List.foreach(List.scala:392) at scala.App.main(App.scala:80) at scala.App.main$(App.scala:78) at IOExample$.main(IOExample.scala:5) at IOExample.main(IOExample.scala)
まとめ
両者の違いを理解して副作用のある処理を安全に書きましょう。